package uk.co.caprica.vlcj.oop.component;

import static uk.co.caprica.vlcj.oop.registry.ObjectName.RENDEZVOUS;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

import uk.co.caprica.vlcj.binding.internal.libvlc_media_t;
import uk.co.caprica.vlcj.logger.Logger;
import uk.co.caprica.vlcj.oop.OutOfProcessMediaPlayer;
import uk.co.caprica.vlcj.oop.process.MediaPlayerProcess;
import uk.co.caprica.vlcj.oop.registry.RemotingRegistry;
import uk.co.caprica.vlcj.oop.util.Classpath;
import uk.co.caprica.vlcj.oop.util.StreamConsumer;
import uk.co.caprica.vlcj.oop.util.SystemProperties;
import uk.co.caprica.vlcj.player.MediaPlayer;
import uk.co.caprica.vlcj.player.MediaPlayerEventListener;
import uk.co.caprica.vlcj.player.events.MediaPlayerEventType;

// FIXME robust clean-up etc...

/**
 * Implementation of an "out-of-process" media player component.
 * <p>
 * This class is the client-side handle to the external media player process.
 * <p>
 * Any public method exposed by this class is potentially invokable by a remote
 * process.
 */
public class OutOfProcessMediaPlayerComponent implements OutOfProcessEventListener, MediaPlayerEventListener {

    /**
     * Name of the host on which the remoting registry resides.
     */
    private static final String registryHost = RemotingRegistry.instance().getHost();
    
    /**
     * Port number at which the remoting registry resides. 
     */
    private static final int registryPort = RemotingRegistry.instance().getPort();
    
    /**
     * Standard set of system properties that should be propagated to the
     * external processes. 
     */
    private static final String[] STANDARD_SYSTEM_PROPERTIES = {
        "java.library.path",
        "jna.library.path",
        "vlcj.log"
    };
    
    /**
     * Thread-safe unique id generator.
     */
    private static final AtomicInteger processIdSequence = new AtomicInteger();

    /**
     * Shared thread-pool.
     * <p>
     * This pool will grow in size as needed. 
     */
    private static final ExecutorService executorService = Executors.newCachedThreadPool();
    
    /**
     * Collection of component event listeners.
     */
    private final List<OutOfProcessEventListener> eventListenerList = new ArrayList<OutOfProcessEventListener>();
    
    /**
     * Unique identifier for this component instance.
     */
    private final int processId;
    
    /**
     * Synchronisation object used to wait for the remote process to be ready,
     * and to get a reference to the out-of-process media player when it is
     * ready. 
     */
    private final OutOfProcessMediaPlayerRendezvous rendezvous;
    
    /**
     * Instance of the external media player process.
     */
    private final Process process;
    
    /**
     * Create a media player component.
     */
    public OutOfProcessMediaPlayerComponent() {
        // Generate a new unique identifier for the process
        this.processId = processIdSequence.getAndIncrement();
        // Create an object to rendezvous with the external process
        this.rendezvous = createRendezvous();
        // Create the external process
        this.process = createProcess();
        // Register listeners
        addOutOfProcessEventListener(this);
    }

    /**
     * 
     * 
     * @param listener
     */
    public final void addOutOfProcessEventListener(OutOfProcessEventListener listener) {
        Logger.debug("addOutOfProcessEventListener(listener={})", listener);
        eventListenerList.add(listener);
    }

    /**
     * 
     * 
     * @param listener
     */
    public final void removeOutOfProcessEventListener(OutOfProcessEventListener listener) {
        Logger.debug("outOfProcessEventListener(listener={})", listener);
        eventListenerList.remove(listener);
    }

    /**
     * Get a reference to the out-of-process media player.
     * 
     * @return media player
     */
    public final OutOfProcessMediaPlayer mediaPlayer() {
        return rendezvous.mediaPlayer();
    }
    
    /**
     * Get a reference to the remote process.
     * 
     * @return process
     */
    public final Process process() {
        return process;
    }
    
    /**
     * Release the resources associated with this component.
     */
    public final void release() {
        Logger.debug("release()");
        onBeforeRelease();
        // FIXME anything else, like remove the registered listener? if the remote process is gone, this will cause problems...
        // Release the remote process and associated resources
        rendezvous.release();
        onAfterRelease();
    }
    
    /**
     * Create a synchronisation object.
     * 
     * @return synchronisation object
     */
    private OutOfProcessMediaPlayerRendezvous createRendezvous() {
        // Create the rendezvous
        OutOfProcessMediaPlayerRendezvous rendezvous = new OutOfProcessMediaPlayerRendezvous(this);
        // Submit the future task used to get the out-of-process media player
        // reference
        executorService.submit(rendezvous.mediaPlayerFuture());
        // Bind the rendezvous instance in the remoting server so the external 
        // process can call back when it has finished its initialisation
        try {
            StringBuilder sb = new StringBuilder();
            sb.append(RENDEZVOUS.objectName()).append(processId);
            String rendezvousBindName = sb.toString();
            Logger.debug("rendezvousBindName={}", rendezvousBindName);
            RemotingRegistry.instance().bind(rendezvous, rendezvousBindName);
        }
        catch(IOException e) {
            throw new RuntimeException("Failed to bind remote media player rendezvous", e);
        }
        // Execute the future task used to get the remote media player from the rendezvous
        executorService.submit(rendezvous.mediaPlayerFuture());
        return rendezvous;
    }
    
    /**
     * Start the external process.
     * 
     * @return process handle
     */
    private Process createProcess() {
        Logger.debug("createProcess()");
        // Create the child process...
        try {
            // Use a builder to create the new process
            ProcessBuilder processBuilder = new ProcessBuilder(getProcessArguments());
            // Redirect the standard error stream to the standard output stream
            processBuilder.redirectErrorStream(true);
            // Start the process
            Process process = processBuilder.start();
            Logger.debug("Started process {}", process);
            // Submit a new task to consume the launched process standard output 
            // and standard error streams - the process will eventually lock up
            // if the streams are not consumed
            executorService.submit(new StreamConsumer(process.getInputStream(), onGetProcessOutputStream(processId)));
            // Submit a new task to monitor the process for termination
            executorService.submit(new WaitForProcessTerminationTask(process));
            // Finally, return the newly created process
            return process;
        }
        catch(IOException e) {
            throw new RuntimeException("Failed to launch child process", e);
        }
    }

    /**
     * Prepare the process arguments that will be used to create the child process.
     * 
     * @return collection of process arguments
     */
    private List<String> getProcessArguments() {
        List<String> processArgs = new ArrayList<String>();
        processArgs.add(onGetJavaExecutable());
        processArgs.addAll(SystemProperties.systemProperties(onGetInheritedSystemProperties()));
        processArgs.add("-cp"); 
        processArgs.add(Classpath.classpath());
        processArgs.add(onGetMainClassName());
        processArgs.add(registryHost); 
        processArgs.add(String.valueOf(registryPort)); 
        processArgs.add(String.valueOf(processId));
        Logger.debug("processArgs={}", processArgs);
        return processArgs;
    }

    /**
     * Get the name of the "java" executable to use to create the new process.
     * <p>
     * The value returned by this function may include a complete file path.
     * 
     * @return executable name
     */
    protected String onGetJavaExecutable() {
        return "java";
    }
    
    /**
     * Get the system properties that should be propagated to the external process.
     * 
     * @return system property names
     */
    protected String[] onGetInheritedSystemProperties() {
        return STANDARD_SYSTEM_PROPERTIES;
    }

    /**
     * Get the fully qualified name of the process implementation class.
     * 
     * @return class name, including package prefix
     */
    protected String onGetMainClassName() {
        return MediaPlayerProcess.class.getName();
    }

    /**
     * Get the output stream to use for the remote process.
     * <p>
     * The standard output of the remote process is redirected to this local 
     * stream.
     * <p>
     * The default behaviour is simply to discard the output.
     * 
     * @return output stream, or <code>null</code> to discard the output
     */
    protected OutputStream onGetProcessOutputStream(int processId) {
        return null;
    }

    /**
     * Get the set of remote media player events that should be enabled.
     * <p>
     * The default behaviour is to enable all events except for {@link MediaPlayerEventType#POSITION_CHANGED}
     * and {@link MediaPlayerEventType#TIME_CHANGED} since those events will fire
     * very frequently.
     * <p>
     * To enable all events, override this method and return {@link MediaPlayerEventType#ALL}.
     * 
     * @return event mask describing the events to enable
     */
    protected int onGetEnabledEvents() {
        return MediaPlayerEventType.notEvents(MediaPlayerEventType.POSITION_CHANGED, MediaPlayerEventType.TIME_CHANGED);
    }
    
    /**
     * 
     */
    protected void onBeforeRelease() {
    }
    
    /**
     * 
     */
    protected void onAfterRelease() {
    }
    
    /**
     * Accept a notification from the rendezvous that the remote process is
     * ready.
     * <p>
     * This is intentionally package-private and is intended only to be invoked
     * by the rendezvous.
     */
    final void notifyProcessReady() {
        Logger.debug("notifyProcessReady()");
        fireProcessStartedEvent();
    }
    
    /**
     * 
     */
    private void fireProcessStartedEvent() {
        Logger.debug("fireProcessStartedEvent()");
        for(int i = eventListenerList.size() - 1; i >= 0; i--) {
            eventListenerList.get(i).processStarted(processId);
        }
    }
    
    /**
     *
     *
     * @param exitCode
     */
    private void fireProcessTerminatedEvent(int exitCode) {
        Logger.debug("fireProcessTerminatedEvent(exitCode={})", exitCode);
        for(int i = eventListenerList.size() - 1; i >= 0; i--) {
            eventListenerList.get(i).processTerminated(processId, exitCode);
        }
    }
    
    /**
     *
     */
    private final class WaitForProcessTerminationTask implements Runnable {

        /**
         * Handle of the process to wait for.
         */
        private final Process process;
        
        /**
         * Create a component that waits for a process to terminate.
         * 
         * @param process
         */
        private WaitForProcessTerminationTask(Process process) {
            this.process = process;
        }
        
        @Override
        public void run() {
            Logger.debug("run()");
            try {
                int result = process.waitFor();
                Logger.debug("Child process {} terminated with result {}", process, result);
                fireProcessTerminatedEvent(result);
            }
            catch(InterruptedException e) {
                Logger.debug("Interrupted waiting for process termination", e);
            }
        }
    }

    // === OutOfProcessEventListener ============================================
    
    @Override
    public final void processStarted(int processId) {
        // FIXME some sort of dead-lock if calling the mediaplayer here...
        
        // Enable only-the sub-set of media player events that should be received
        // from the out-of-process media player - each event callback is a remote
        // call and some events will be fired very often (like time-changed or
        // position changed)
//        mediaPlayer().enableEvents(onGetEnabledEvents());
        // Register for remote media player event listeners
//        mediaPlayer().addMediaPlayerEventListener(OutOfProcessMediaPlayerComponent.this);
        onProcessStarted(processId);
    }

    @Override
    public final void processTerminated(int processId, int exitCode) {
        // FIXME
        // Do something? like release()?
        // Invoke the template method
        onProcessTerminated(processId, exitCode);
    }

    /**
     * 
     * 
     * @param processId
     */
    protected void onProcessStarted(int processId) {
    }

    /**
     * 
     * 
     * @param processId
     * @param exitCode
     */
    protected void onProcessTerminated(int processId, int exitCode) {
    }

    // === MediaPlayerEventListener =============================================

    @Override
    public void mediaChanged(MediaPlayer mediaPlayer) {
    }

    @Override
    public void opening(MediaPlayer mediaPlayer) {
    }

    @Override
    public void buffering(MediaPlayer mediaPlayer) {
    }

    @Override
    public void playing(MediaPlayer mediaPlayer) {
    }

    @Override
    public void paused(MediaPlayer mediaPlayer) {
    }

    @Override
    public void stopped(MediaPlayer mediaPlayer) {
    }

    @Override
    public void forward(MediaPlayer mediaPlayer) {
    }

    @Override
    public void backward(MediaPlayer mediaPlayer) {
    }

    @Override
    public void finished(MediaPlayer mediaPlayer) {
    }

    @Override
    public void timeChanged(MediaPlayer mediaPlayer, long newTime) {
    }

    @Override
    public void positionChanged(MediaPlayer mediaPlayer, float newPosition) {
    }

    @Override
    public void seekableChanged(MediaPlayer mediaPlayer, int newSeekable) {
    }

    @Override
    public void pausableChanged(MediaPlayer mediaPlayer, int newSeekable) {
    }

    @Override
    public void titleChanged(MediaPlayer mediaPlayer, int newTitle) {
    }

    @Override
    public void snapshotTaken(MediaPlayer mediaPlayer, String filename) {
    }

    @Override
    public void lengthChanged(MediaPlayer mediaPlayer, long newLength) {
    }

    @Override
    public void videoOutput(MediaPlayer mediaPlayer, int newCount) {
    }

    @Override
    public void error(MediaPlayer mediaPlayer) {
    }

    @Override
    public void mediaMetaChanged(MediaPlayer mediaPlayer, int metaType) {
    }

    @Override
    public void mediaSubItemAdded(MediaPlayer mediaPlayer, libvlc_media_t subItem) {
    }

    @Override
    public void mediaDurationChanged(MediaPlayer mediaPlayer, long newDuration) {
    }

    @Override
    public void mediaParsedChanged(MediaPlayer mediaPlayer, int newStatus) {
    }

    @Override
    public void mediaFreed(MediaPlayer mediaPlayer) {
    }

    @Override
    public void mediaStateChanged(MediaPlayer mediaPlayer, int newState) {
    }

    @Override
    public void newMedia(MediaPlayer mediaPlayer) {
    }

    @Override
    public void subItemPlayed(MediaPlayer mediaPlayer, int subItemIndex) {
    }

    @Override
    public void subItemFinished(MediaPlayer mediaPlayer, int subItemIndex) {
    }

    @Override
    public void endOfSubItems(MediaPlayer mediaPlayer) {
    }
}
